#include <caml/mlvalues.h>
#include <caml/alloc.h>
#include <caml/memory.h>
#include <caml/callback.h>
#include <caml/fail.h>
#include <caml/custom.h>

#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
#include <libavutil/avstring.h>

#include <GL/gl.h>
#include <GL/glu.h>

typedef unsigned char boolean;

typedef struct
{
    AVFormatContext* formatCtx;
    AVCodecContext* codecCtx;
    AVCodec* codec;
    int vidStr;
    AVPacket packet;
    AVFrame* frame;
    int bytesRemaining;
    unsigned char* rawData;
    boolean firstTime;
    struct SwsContext* swsctx;
    int p2width;
    int p2height;
    unsigned char* outbuff;
} video_t;

typedef struct
{
    video_t* video;
    int freeable;
    void (*finalizer)(void*);
    void* finalizer_data;
} ml_video_t;

video_t* VIDEO(value s)
{
    ml_video_t* cb_data;
    
    if(Tag_val(s) == 0)
        cb_data = Data_custom_val(Field(s, 0));
    else
        cb_data = Data_custom_val(s);

    return cb_data->video;
}

static void ml_vr_close_video(value s)
{
    ml_video_t* cb_data;
    int call_finalizer = 0;

    if(Tag_val(s) == 0)
        cb_data = Data_custom_val(Field(s, 0));
    else
        cb_data = Data_custom_val(s);

    if(cb_data->finalizer)
        call_finalizer = 1;

    if(cb_data->freeable)
    {
        free(cb_data->video->outbuff);
        sws_freeContext(cb_data->video->swsctx);
        av_free(cb_data->video->frame);
        avcodec_close(cb_data->video->codecCtx);
        av_close_input_file(cb_data->video->formatCtx);
        free(cb_data->video);
    }

    if(call_finalizer)
        cb_data->finalizer(cb_data->finalizer_data);
}

int ml_vr_video_compare(value v1, value v2)
{
    video_t* vi1 = VIDEO(v1);
    video_t* vi2 = VIDEO(v2);
    if(vi1 == vi2) return 0;
    if(vi1 < vi2) return -1;
    return 1;
}

static struct custom_operations video_t_ops = {
    "video",
    &ml_vr_close_video,
    &ml_vr_video_compare,
    NULL,
    NULL,
    NULL
};

#define ML_VIDEO(v) Val_Video((v), 1, Val_unit, NULL, NULL)
value Val_Video(video_t* video, int freeable, value barr,
                void (*finalizer)(void*), void* data)
{
    CAMLparam1(barr);
    CAMLlocal1(v);
    ml_video_t* cb_data;
    v = alloc_custom(&video_t_ops, sizeof(*cb_data), 100000, 1000000000);
    cb_data = Data_custom_val(v);
    cb_data->video = video;
    cb_data->freeable = freeable;
    cb_data->finalizer = finalizer;
    cb_data->finalizer_data = data;
    CAMLreturn(v);
}

unsigned int np2(unsigned int a)
{
    unsigned int p = 1;
    while(p < a)
        p *= 2;
    return p;
}

boolean get_next_frame(video_t* vid)
{
    int bytesDecoded;
    int frameFinished;

    if(vid->firstTime)
    {
        vid->firstTime = 0;
        vid->packet.data = NULL;
    }

    for(;;)
    {
        while(vid->bytesRemaining > 0)
        {
            bytesDecoded = avcodec_decode_video(vid->codecCtx, vid->frame,
                &frameFinished, vid->rawData, vid->bytesRemaining);

            if(bytesDecoded < 0)
            {
                caml_failwith("error decoding stream");
            }

            vid->bytesRemaining -= bytesDecoded;
            vid->rawData += bytesDecoded;

            if(frameFinished)
                return 1;
        }

        do
        {
            if(vid->packet.data != NULL)
                av_free_packet(&vid->packet);

            if(av_read_packet(vid->formatCtx, &vid->packet)<0)
                goto loop_exit;
        } while(vid->packet.stream_index != vid->vidStr);

        vid->bytesRemaining = vid->packet.size;
        vid->rawData = vid->packet.data;
    }

loop_exit:
    bytesDecoded = avcodec_decode_video(vid->codecCtx, vid->frame, &frameFinished, 
        vid->rawData, vid->bytesRemaining);

    if(vid->packet.data != NULL)
        av_free_packet(&vid->packet);

    return frameFinished!=0;
}

CAMLprim value ml_vr_init(value unit)
{
    av_log_set_level(AV_LOG_DEBUG);
    av_register_all();
    return Val_unit;
}

CAMLprim value ml_vr_open_video(value path)
{
    AVFormatParameters params, *p = &params;
    char* file = String_val(path);
    video_t* v = malloc(sizeof(video_t));
    int i;

    v->bytesRemaining = 0;
    v->firstTime = 1;

    v->formatCtx = av_alloc_format_context();
    if(av_open_input_file(&v->formatCtx, file, NULL, 0, p) < 0)
    {
        caml_failwith("unable to open file");
    }

    if(av_find_stream_info(v->formatCtx) < 0)
    {
        caml_failwith("unable to find stream in file");
    }

    v->vidStr = -1;
    for(i = 0; i < v->formatCtx->nb_streams; ++i)
    {
        if(v->formatCtx->streams[i]->codec->codec_type == CODEC_TYPE_VIDEO)
        {
            v->vidStr = i;
            break;
        }
    }
    if(v->vidStr == -1)
    {
        caml_failwith("unable to find a video stream in file");
    }
    v->codecCtx = v->formatCtx->streams[v->vidStr]->codec;

    v->codec = avcodec_find_decoder(v->codecCtx->codec_id);
    if(!v->codec)
    {
        caml_failwith("no codec available for the video stream");
    }

    if(v->codec->capabilities & CODEC_CAP_TRUNCATED)
        v->codecCtx->flags |= CODEC_FLAG_TRUNCATED;

    if(avcodec_open(v->codecCtx, v->codec) < 0)
    {
        caml_failwith("unable to open codec");
    }

    v->frame = avcodec_alloc_frame();

    v->p2width = np2(v->codecCtx->width);
    v->p2height = np2(v->codecCtx->height);

    v->swsctx = sws_getContext(
            v->codecCtx->width, v->codecCtx->height, v->codecCtx->pix_fmt,
            v->p2width, v->p2height, PIX_FMT_BGR32,
            SWS_FAST_BILINEAR, NULL, NULL, NULL);

    v->outbuff = malloc(avpicture_get_size(PIX_FMT_RGB32,
                v->p2width, v->p2height));

    return ML_VIDEO(v);
}

CAMLprim value ml_vr_gl_texture_of_frame(value vid)
{
    video_t* v = VIDEO(vid);
    unsigned char* pixels[4];
    int linesize[4];
    int ret;
    GLuint id;

    ret = get_next_frame(v);
    if(!ret)
    {
        return Val_int(-1);
    }

    pixels[0] = v->outbuff;
    pixels[1] = 0;
    pixels[2] = 0;
    pixels[3] = 0;

    linesize[0] = v->p2width * 4;
    linesize[1] = 0;
    linesize[2] = 0;
    linesize[3] = 0;

    sws_scale(v->swsctx, v->frame->data, v->frame->linesize, 0,
            v->p2height, pixels, linesize);

    glGenTextures(1, &id);
    glBindTexture(GL_TEXTURE_2D, id);

#if 0
    gluBuild2DMipmaps(GL_TEXTURE_2D, 4, np2(v->codecCtx->width),
            np2(v->codecCtx->height), GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
#else
    glTexImage2D(GL_TEXTURE_2D, 0, 4, v->p2width, v->p2height,
            0, GL_RGBA, GL_UNSIGNED_BYTE, v->outbuff);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
#endif

    return Val_int(id);
}
